function getUID(length) {
    if (!length) length = 5;
    return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36);
}

// 获取本地化语言
function api_locales(message, placeholders) {
    var res = chrome.i18n.getMessage(message, placeholders);
    return res ? res : message;
}

function api_runtime_on_message_listener(callback) {
    chrome.runtime.onMessage.addListener(callback);
}

var messageAsyncCallbackRegister = {};
function api_send_message(message, callback) {
    message['UID'] = getUID();
    if (message.hasOwnProperty('callback')) {
        messageAsyncCallbackRegister[message['UID']] = message.callback;
    }
    chrome.runtime.sendMessage(message, callback)
}
function api_send_callback_message(sender, fromMessage, returnMessage) {
    if (typeof returnMessage != 'object') api_send_callback_message = {};
    returnMessage['from'] = fromMessage['type'];
    returnMessage['type'] = 'messageCallback';
    returnMessage['UID'] = fromMessage['UID'];
    api_send_tab_message(sender.tab.id, returnMessage)
}
function api_message_callback(message) {
    if (messageAsyncCallbackRegister.hasOwnProperty(message['UID'])) {
        messageAsyncCallbackRegister[message['UID']] && messageAsyncCallbackRegister[message['UID']](message);
        delete messageAsyncCallbackRegister[message['UID']];
    }
}

function api_send_tab_message(tabID, message, callback) {
    chrome.tabs.sendMessage(tabID, message, callback)
}

function api_tab_captureVisibleTab(callback) {
    chrome.tabs.captureVisibleTab(callback)
}

function api_storage_sync_set(data, callback) {
    chrome.storage.sync.set(data, callback)
}

function api_storage_sync_get(data, callback) {
    chrome.storage.sync.get(data, callback)
}

function api_storage_sync_remove(data, callback) {
    chrome.storage.sync.remove(data, callback)
}

function api_storage_local_remove(data, callback) {
    chrome.storage.local.remove(data, callback)
}

function api_storage_local_set(data, callback) {
    chrome.storage.local.set(data, callback)
}

function api_storage_local_get(data, callback) {
    chrome.storage.local.get(data, callback)
}

function api_storage_local_remove(data, callback) {
    chrome.storage.local.get(data, callback)
}

function api_storage_on_change_listener(callback) {
    chrome.storage.onChanged.addListener(callback)
}

function api_bookmark_get(idOrIdList, callback) {
    chrome.bookmarks.get(idOrIdList, callback);
}

function api_bookmark_query(query, callback) {
    chrome.bookmarks.search(query, callback);
}

function api_bookmark_getRecent(numberOfItems, callback) {
    chrome.bookmarks.getRecent(numberOfItems, callback);
}

function api_bookmark_getChildren(stringID, callback) {
    chrome.bookmarks.getChildren(stringID, callback);
}

function api_history_query(query, callback) {
    chrome.history.search(query, callback);
}

function api_notifications_create(options, callback) {
    chrome.notifications.create(options, callback);
}

function eval_inject_code(code) {
    return code
}

function api_execute_script(tabId, details, callback) {
    chrome.scripting.executeScript({
        target: {tabId: tabId},
        func: eval_inject_code,
        args: [details.code]
    }, callback);
}

function api_manifest() {
    return chrome.runtime.getManifest()
}

function api_enterprise_networkDetails(callback) {
    chrome.enterprise.networkingAttributes.getNetworkDetails(callback)
}

function api_sessions_getDevices(filter, callback) {
    chrome.sessions.getDevices(filter, callback)
}

function api_sessions_getRecentlyClosed(filter, callback) {
    chrome.sessions.getRecentlyClosed(filter, callback)
}

function api_sessions_restore(sessionId, callback) {
    chrome.sessions.restore(sessionId !== null ? sessionId + '' : sessionId, callback)
}

function api_get_available_static_rule_count(callback) {
    return chrome.declarativeNetRequest.getAvailableStaticRuleCount(callback)
}

function api_get_dynamic_rules(callback) {
    return chrome.declarativeNetRequest.getDynamicRules(callback)
}

function api_get_enabled_rulesets(callback) {
    return chrome.declarativeNetRequest.getEnabledRulesets(callback)
}

function api_get_matched_rules(filter, callback) {
    return chrome.declarativeNetRequest.getMatchedRules(filter, callback)
}

function api_get_session_rules(callback) {
    return chrome.declarativeNetRequest.getSessionRules(callback)
}

function api_is_regex_supported(regexOptions, callback) {
    return chrome.declarativeNetRequest.isRegexSupported(regexOptions, callback)
}

function api_set_extension_action_options(options, callback) {
    return chrome.declarativeNetRequest.setExtensionActionOptions(options, callback)
}

function api_test_match_outcome(request, callback) {
    return chrome.declarativeNetRequest.testMatchOutcome(request, callback)
}

function api_update_dynamic_rules(options, callback) {
    return chrome.declarativeNetRequest.updateDynamicRules(options, callback)
}

function api_update_enabled_rulesets(options, callback) {
    return chrome.declarativeNetRequest.updateEnabledRulesets(options, callback)
}

function api_update_session_rules(options, callback) {
    return chrome.declarativeNetRequest.updateSessionRules(options, callback)
}

function api_cookie_get(details, callback) {
    return chrome.cookies.get(details, callback);
}

function api_cookie_getAll(details, callback) {
    return chrome.cookies.getAll(details, callback);
}

function api_cookie_getAllCookieStores(callback) {
    return chrome.cookies.getAllCookieStores(callback);
}

function api_cookie_remove(details, callback) {
    return chrome.cookies.remove(details, callback);
}

function api_cookie_set(details, callback) {
    return chrome.cookies.set(details, callback);
}
function api_getURL(url) {
    return chrome.runtime.getURL(url)
}

function api_getIcon(url, size) {
    if (!size) size = 16;
    
    return "_favicon/?page_url="+url+"&size=" + size;
}

function api_locale_language() {
    return chrome.i18n.getUILanguage();
}


function api_tab_onupdated(callback) {
    chrome.tabs.onUpdated.addListener(callback);
}

function api_tab_goForward(tabId, callback) {
    chrome.tabs.goForward(tabId, callback);
}

function api_tab_goBack(tabId, callback) {
    chrome.tabs.goBack(tabId, callback);
}

function api_tab_copy(tabId, callback) {
    if (tabId) {
        chrome.tabs.duplicate(tabId, callback);
    } else {
        api_tab_current(function (tab) {
            chrome.tabs.duplicate(tab[0].id, callback);
        });
    }
}

function api_tab_create(url, callback) {
    let config = typeof url == 'object' ? url : { url: url };
    chrome.tabs.create(config, callback);
}

function api_tab_get(tabId, callback) {
    chrome.tabs.get(tabId, callback);
}

function api_tab_update(tabId, data, callback) {
    chrome.tabs.update(tabId, data, callback)
}

function api_tab_current(callback) {
    api_tab_query({ active: true, currentWindow: true }, callback);
}

function api_tab_query(query, callback) {
    chrome.tabs.query(query, callback);
}

function api_tab_remove(tabIds, callback) {
    chrome.tabs.remove(tabIds, callback)
}

function api_window_create(createData, callback) {
    chrome.windows.create(createData, callback)
}

function api_window_current(data, callback) {
    chrome.windows.getCurrent(data, callback)
}

function api_window_update(windowId, updateInfo, callback) {
    chrome.windows.update(windowId, updateInfo, callback)
}

function getPropertyVal(object, property, defaultVal) {
    if (object && object.hasOwnProperty(property)) {
        return object[property];
    }
    return defaultVal;
}

function checkObjectIsEmpty(obj) {
    if (obj) {
        for (var i in obj) {
            return false;
        }
    }
    return true;
}

function getUrlParamVal(name, url) {
    if (!url) url = location.href;
    name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
    var regexS = "[\\?&]"+name+"=([^&#]*)";
    var regex = new RegExp( regexS );
    var results = regex.exec( url );
    return results == null ? null : results[1];
}

function formateTime(date) {
    var year = date.getFullYear();
    var month = Pad(date.getMonth() + 1, 2);    //js从0开始取 
    var day = Pad(date.getDate(), 2);
    var hour = Pad(date.getHours(), 2);
    var minutes = Pad(date.getMinutes(), 2);
    var second = Pad(date.getSeconds(), 2);
    return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + second;
}

function Pad(num, n, right) {
    var len = num.toString().length;
    while (len < n) {
        if (right) num = num + "0";
        else num = "0" + num;
        len++;
    }
    return num;
}

function Sleep(delay) {
    new Promise(function(resolve) {
        setTimeout(resolve, delay);
    })
}

function IsJsonString(str) {
    try {
        if (typeof JSON.parse(str) == "object") {
            return true;
        }
    } catch (e) {}
    return false;
}

function isFunction(value) { return typeof value == "function" }
function isWindow(obj)     { return obj != null && obj == obj.window }
function isDocument(obj)   { return obj != null && obj.nodeType == obj.DOCUMENT_NODE }
function isObject(obj)     { return typeof obj == "object" }

function isPlainObject(o) {
    return isObject(o) && !isWindow(o) && Object.getPrototypeOf(o) == Object.prototype
}

function stringify(obj) {
    let str = '';
    let strList = [];
    obj.forEach(function(item, key) {
        strList.push(`${key}=${item}`);
    })
    return strList.join('&');
}

function fetchios(options) {
    let url = options.url;
    // init params
    if (typeof url !== 'string') throw new TypeError('url must be required and of string type');

    let responseType = options.responseType || 'json';
    let body = options.data || null
    let timeout = options.timeout || 0
    let config = {
        method: options.method.toUpperCase()
    };

    if (options.headers) config.headers = options.headers;
    if (options.cache) config.cache = options.cache;
    if (options.credentials) config.credentials = options.credentials;

    // 处理URL：请求前缀 & 问号参数
    if (body != null) {
        // 根据自己的需求来:body传递的是普通对象，我们今天项目需要传递给服务器的是URLENCODED格式，我们才处理它的格式；如果用户传递的本身就不是普通对象(例如:文件流、字符串、FORM-DATA...)，还是以用户自己写的为主...
        if (isPlainObject(body)) {
            config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
            body = stringify(body);
        } else if (typeof body === 'string') {
            try {
                // 是JSON字符串
                body = JSON.parse(body);
                body = JSON.stringify(body)
                config.headers['Content-Type'] = 'application/json';
            } catch (err) {
                // 不是JSON字符串:可以简单粗暴的按照URLECCODED格式字符串处理
                config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
            }
        }
        if (config.method == "GET") {
            url += `${url.includes('?')?'&':'?'}${body}`;
        }
    }

    if (/^(POST|PUT|PATCH)$/i.test(config.method) && body != null) config.body = body;

    // timeout
    let timeoutId = 0;
    if (timeout > 0) {
        const controller = new AbortController();
        timeoutId = setTimeout(() => controller.abort(), timeout);
        config.signal = controller.signal
    }

    // 发送请求
    return fetch(url, config).then(response => {
        clearTimeout(timeoutId);
        let {
            status,
            statusText
        } = response;

        let ret = {
            status,
            statusText,
            response: null
        }
        
        // 只要状态码是以2或者3开始的，才是真正的获取成功
        if (status >= 200 && status < 400) {
            switch (responseType.toUpperCase()) {
                case 'JSON':
                    ret.response = response.json();
                break;
                case 'TEXT':
                    ret.response = response.text();
                break;
                case 'BLOB':
                    ret.response = response.blob();
                break;
                case 'ARRAYBUFFER':
                    ret.response = response.arrayBuffer();
                break;
                default:
                    ret.response = response.text();
                break;
            }
            return ret.response;
        }
        return Promise.reject(ret);
    }).catch(reason => {
        clearTimeout(timeoutId);
        if (reason && reason.status > 0) {
            // @1 状态码错误
        } else if (!navigator.onLine) {
            // @2 网络中断
            reason.statusText = 'NETWORK BLOCK'
        } else {
            // @3 请求被终止
            reason.statusText = 'REQUEST STOP'
        }
        return Promise.reject(reason);
    });
}
/**
 * cron.js
 * ---
 * VERSION 0.1
 * ---
 * DON'T USE THIS -- IT'S NOT QUITE THERE YET!
 * ---
 * @author James Padolsey
 * ---
 * Dual licensed under the MIT and GPL licenses.
 *    - http://www.opensource.org/licenses/mit-license.php
 *    - http://www.gnu.org/copyleft/gpl.html
 */
var CronJob = (function(){
    
    var undefined;
    var intervalHandle = null;
    
    function CronTime(time) {
        
        this.source = time;
        
        this.map = ['second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'];
        this.constraints = [[0,59],[0,59],[0,23],[1,31],[0,11],[0,6]];
        this.aliases = {
            jan:1,feb:2,mar:3,apr:4,may:5,jun:6,jul:7,aug:8,sep:9,oct:10,nov:11,dec:12,
            sun:0,mon:1,tue:2,wed:3,thu:4,fri:5,sat:6
        };
        
        this.second = {};
        this.minute = {};
        this.hour = {};
        this.dayOfMonth = {};
        this.month = {};
        this.dayOfWeek = {};
        
        this._parse();
        
    };
    
    CronTime.prototype = {
        _parse: function() {
            
            var aliases = this.aliases,
                source = this.source.replace(/[a-z]/i, function(alias){
                    
                    alias = alias.toLowerCase();
                    
                    if (alias in aliases) {
                        return aliases[alias];
                    }
                    
                    throw new Error('Unknown alias: ' + alias);
                    
                }),
                split = this.source.replace(/^\s\s*|\s\s*$/g, '').split(/\s+/),
                cur, len = 6;
            
            while (len--) {
                cur = split[len] || '*';
                this._parseField(cur, this.map[len], this.constraints[len]);
            }
            
        },
        _parseField: function(field, type, constraints) {
            
            var rangePattern = /(\d+?)(?:-(\d+?))?(?:\/(\d+?))?(?:,|$)/g,
                typeObj = this[type],
                diff,
                low = constraints[0],
                high = constraints[1];
            
            // * is a shortcut to [lower-upper] range
            field = field.replace(/\*/g,  low + '-' + high);
                
            if (field.match(rangePattern)) {
                
                field.replace(rangePattern, function($0, lower, upper, step) {
                    
                    step = step || 1;
                    
                    // Positive integer higher than constraints[0]
                    lower = Math.max(low, ~~Math.abs(lower));
                    
                    // Positive integer lower than constraints[1]
                    upper = upper ? Math.min(high, ~~Math.abs(upper)) : lower;
                    
                    diff = step + upper - lower;
                    
                    while ((diff-=step) > -1) {
                        typeObj[diff + lower] = true;
                    }
                    
                });
                
            } else {
                
                throw new Error('Field (' + field + ') cannot be parsed');
                
            }
            
        }
    };
    
    function CronJob(cronTime, event) {
        
        if (!(this instanceof CronJob)) {
            return new CronJob(cronTime, event);
        }
        
        this.events = [event];
        this.cronTime = new CronTime(cronTime);
        this.now = {};
        this.initiated = false;
        
        this.clock();
        
    }
    
    CronJob.prototype = {
        
        addEvent: function(event) {
            this.events.push(event);
        },
        
        runEvents: function() {
            for (var i = -1, l = this.events.length; ++i < l; ) {
                if (typeof this.events[i] === 'function') {
                    this.events[i]();
                }
            }
        },

        Stop: function() {
            clearInterval(this.intervalHandle);
        },
        
        clock: function() {
            
            var date = new Date,
                now = this.now,
                self = this,
                cronTime = this.cronTime,
                i;
            
            if (!this.initiated) {
                // Make sure we start the clock precisely ON the 0th millisecond
                setTimeout(function(){
                    self.initiated = true;
                    self.clock();
                }, Math.ceil(+date / 1000) * 1000 - +date);
                return;
            }
            
            this.timer = this.timer || (this.intervalHandle = setInterval(function(){self.clock();}, 1000));
            
            now.second = date.getSeconds();
            now.minute = date.getMinutes();
            now.hour = date.getHours();
            now.dayOfMonth = date.getDate();
            now.month = date.getMonth();
            now.dayOfWeek = date.getDay();
            
            for (i in now) {
                if (!(now[i] in cronTime[i])) {
                    return;
                }
            }
            
            this.runEvents();
            
        }
        
    };
    
    return CronJob;
    
})();
function bookmark_handle(message, callback) {
    switch (message.method) {
        case "query":
            bookmark_query_handle(message, callback);
            break;
        case "getRecent":
            bookmark_getRecent_handle(message, callback);
            break;
        case "list":
            bookmark_list_handle(message, callback);
            break;
    }
}

function bookmark_query_handle(message, callback) {
    api_bookmark_query(message.content.length > 0 ? message.content[0] : '', callback);
}

function bookmark_getRecent_handle(message, callback) {
    api_bookmark_getRecent(message.options.numberOfItems, callback);
}

function bookmark_list_handle(message, callback) {
    var folder = message.content.length > 0 ? message.content[0] : '';
    folder = folder ? folder+''.trim() : '0';
    api_bookmark_getChildren(folder, function (results) {
        // get top
        bookmark_get_path(folder, [], function (transparent) {
            callback({ path: transparent, bookmark: results });
        });

    })
}

function bookmark_get_path(id, transparent, callback) {
    if (!transparent) transparent = [];
    api_bookmark_get(id, function (nodes) {
        console.log(nodes)
        if (nodes && nodes.length > 0) {
            transparent.unshift(nodes[0]);
            if (nodes[0].hasOwnProperty('parentId')) {
                bookmark_get_path(nodes[0].parentId, transparent, callback);
            } else {
                callback && callback(transparent);
            }
        } else {
            callback && callback(transparent);
        }
    })
}
var CmdHub = {};
function message_cmdhub(message, sender, callback) {
    let meta = { code: 200, msg: "", data: [] };
    switch (message.method) {
        case 'get_cmdhub':
            meta.data = CmdHub;
            api_send_callback_message(sender, message, { meta: meta });
            break;
        case 'add':
            newCmd = message.newCmd;
            if (CmdHub.hasOwnProperty(newCmd)) {
                meta.code = 300;
                meta.msg = 'The command ' + newCmd + ' has exist.';
                api_send_callback_message(sender, message, { meta: meta });
            } else {
                cmdhub_get_cmd_code(newCmd, function (meta, config, code) {
                    cmdhub_update_all_tabs_cmd(newCmd);
                    api_send_callback_message(sender, message, { meta: meta });
                }, function (meta) {
                    api_send_callback_message(sender, message, { meta: meta });
                }, getPropertyVal(message.options, "hub"), null);
            }

            break;
        case 'update':
            newCmd = message.newCmd;
            cmdhub_get_cmd_code(newCmd, function (meta, config, code) {
                cmdhub_update_all_tabs_cmd(newCmd);
                api_send_callback_message(sender, message, { meta: meta });
            }, function (meta) {
                api_send_callback_message(sender, message, { meta: meta });
            }, getPropertyVal(message.options, "hub"), null);
            break;
        case 'delete':
            newCmd = message.newCmd;
            delete CmdHub[newCmd];
            api_storage_sync_set({ 'cmd:hub': CmdHub });
            meta.data = CmdHub;
            api_send_callback_message(sender, message, { meta: meta });
            break;
        case 'custom':
            if (message.hasOwnProperty('newCmd') && message.hasOwnProperty('cmdContent')) {
                let cmdset = {};
                newCmd = message.newCmd;
                cmdset['cmd:code:' + newCmd] = message.cmdContent;
                CmdHub[newCmd] = {
                    version: '1.0.0',
                    site: '*',
                    index_func: newCmd + 'Cmd',
                }
                api_storage_sync_set({ 'cmd:hub': CmdHub });
                api_storage_sync_set(cmdset);
                cmdhub_update_all_tabs_cmd(newCmd);
                meta.data = CmdHub;
                api_send_callback_message(sender, message, { meta: meta });
            }
            break;
    }
}

function cmdhub_get_cmd_code(newCmd, successCallback, errorCallback, cmdhub_prefix) {
    if (!cmdhub_prefix) cmdhub_prefix = 'https://raw.githubusercontent.com/web-terminal/cmdhub/master/';
    let cmdhub_source_prefix = 'https://github.com/web-terminal/cmdhub/blob/master/';
    if (cmdhub_prefix.substr(-1, 1) != '/') cmdhub_prefix += '/';
    if (cmdhub_source_prefix.substr(-1, 1) != '/') cmdhub_source_prefix += '/';
    let meta = { code: 200, msg: "", data: [] };
    let cmdJsonUrl = cmdhub_prefix + newCmd + '/cmd.json?time=' + (new Date().getTime())
    $.ajax({
        url: cmdJsonUrl,
        type: 'GET',
        async: true,
        dataType: 'json',
        success: function (jsonData) {
            if (jsonData.hasOwnProperty('version')) {
                let mainFile = jsonData.hasOwnProperty('main') ? jsonData.main : 'main.js';
                // get main file
                $.ajax({
                    url: cmdhub_prefix + newCmd + '/' + mainFile + '?time=' + (new Date().getTime()),
                    type: 'GET',
                    async: true,
                    dataType: 'text',
                    success: function (jsFile) {
                        let cmdset = {}
                        cmdset['cmd:code:' + newCmd] = jsFile;
                        CmdHub[newCmd] = {
                            version: jsonData.version,
                            site: jsonData.hasOwnProperty('site') ? jsonData.site : '',
                            index_func: jsonData.index_func,
                            init_func: getPropertyVal(jsonData, 'init_func', ''),
                        }

                        api_storage_sync_set({ 'cmd:hub': CmdHub });
                        api_storage_local_set(cmdset);
                        meta.data = CmdHub;
                        successCallback && successCallback(meta, jsonData, cmdset['cmd:code:' + newCmd]);
                    },
                    error: function (xhr) {
                        meta.code = 500;
                        meta.msg = 'Get code error. please check you have visit url <a href="' + cmdhub_source_prefix + newCmd + '/' + mainFile + '">' + cmdhub_source_prefix + newCmd + '/' + mainFile + '</a>';
                        errorCallback && errorCallback(meta, jsonData);
                    }
                });
            } else {
                meta.code = 500;
                meta.msg = 'Invalid command config, unknown command version.';
                errorCallback && errorCallback(meta, jsonData);
            }
        },
        error: function (xhr) {
            meta.code = 500;
            meta.msg = 'Please check that you can visit <a target="_blank" href="' + cmdJsonUrl + '">' + newCmd + '</a> address.';
            errorCallback && errorCallback(meta, null);
        }
    });
}

function cmdhub_update_all_tabs_cmd(cmd) {
    api_tab_query({}, function (tabs) {
        for (let i in tabs) {
            if (tabs[i].url.startsWith('http')) {
                cmdhub_inject_cmd(cmd, tabs[i].id);
            }
        }
    });
}

function cmdhub_inject_cmd(cmd, tabId, callback, error) {
    let cmd_storage_key = 'cmd:code:' + cmd;
    try {
        api_storage_local_get(cmd_storage_key, function (res) {
            if (res && res.hasOwnProperty(cmd_storage_key)) {
                api_execute_script(tabId, { code: res[cmd_storage_key] }, callback);
            } else {
                cmdhub_get_cmd_code(cmd, function (meta, config, code) {
                    api_execute_script(tabId, { code: code }, callback);
                });
            }
        });
    } catch (e) {
        typeof error == 'function' && error(e);
    }

}

function cmdhub_init(tabId) {
    api_storage_sync_get(['cmd:hub'], function (res) {
        if (res && res.hasOwnProperty('cmd:hub')) {
            CmdHub = res['cmd:hub'];
            for (let cmd in CmdHub) {
                cmdhub_inject_cmd(cmd, tabId);
            }
        }
    });

}

function cmdhub_listen_tab_loading_event(tabId, changeInfo, tab) {
    if (changeInfo.hasOwnProperty('status') && changeInfo.status == 'loading' && tab.url.startsWith('http')) {
        cmdhub_init(tabId)
    }
}
function message_cookie(message, sender) {
    // 将storeId转换成string
    if (message.config.hasOwnProperty('storeId')) {
        message.config.storeId += '';
    }
    switch (message.method) {
        case 'get':
            api_cookie_get(message.config, function(cookie) {
                api_send_callback_message(sender, message, { 'data': cookie })
            });
        break;
        case 'getAll':
            api_cookie_getAll(message.config, function(cookies) {
                api_send_callback_message(sender, message, { 'data': cookies })
            });
        break;
        case 'getAllCookieStores':
            api_cookie_getAllCookieStores(function(cookieStores) {
                api_send_callback_message(sender, message, { 'data': cookieStores })
            });
        break;
        case 'remove':
            api_cookie_remove(message.config, function(details) {
                api_send_callback_message(sender, message, { 'data': details })
            });
        break;
        case 'set':
            api_cookie_set(message.config, function(cookie) {
                api_send_callback_message(sender, message, { 'data': cookie })
            });
        break;
    }
}
// 初始化cron
var cronJobStacks = {};
var cronJobMaps = {};
api_storage_sync_get("cron-jobs", function (results) {
    if (results.hasOwnProperty("cron-jobs")) {
        cronJobMaps = results["cron-jobs"];
        for (let id in cronJobMaps) {
            if (cronJobMaps[id].enabled) {
                cronItemRunStart(cronJobMaps[id]);
            }
        }
    }
});

function addCronItem(taburl, options) {
    // 对url去除锚点
    let archPos = taburl.indexOf('#');
    if (taburl.indexOf('#') > 0) {
        taburl = taburl.substring(0, archPos)
    }
    let item = {
        id: getUID(),
        rule: options.rule,
        cmds: options.cmds,
        url: taburl,
        openType: options.hasOwnProperty('openType') ? options.openType : "auto-open",  // auto-open, open-only 
        showType: options.hasOwnProperty('showType') ? options.showType : "frontend", // background, frontend
        enabled: true,  // true, false
        times: 0
    }
    cronJobMaps[item.id] = item;
    cronItemRunStart(item);
    api_storage_sync_set({ "cron-jobs": cronJobMaps });
}
function updateCronItem(options) {
    if (options.hasOwnProperty('id') && cronJobMaps.hasOwnProperty(options.id)) {
        let id = options.id;
        if (options.hasOwnProperty('rule')) {
            cronJobMaps[id]['rule'] = options.rule;
            cronItemRunStop(cronJobMaps[id]);
            if (!options.hasOwnProperty('enabled'))
                options['enabled'] = cronJobMaps[id]['enabled'];
        }
        // return
        if (options.hasOwnProperty('cmds')) cronJobMaps[id]['cmds'] = options.cmds;
        if (options.hasOwnProperty('showType')) cronJobMaps[id]['showType'] = options.showType;
        if (options.hasOwnProperty('openType')) cronJobMaps[id]['openType'] = options.openType;
        if (options.hasOwnProperty('enabled')) {
            cronJobMaps[id]['enabled'] = options.enabled;
            if (options.enabled) {
                cronItemRunStart(cronJobMaps[id]);
            } else {
                cronItemRunStop(cronJobMaps[id]);
            }
        }
        api_storage_sync_set({ "cron-jobs": cronJobMaps });
    }
}
function removeCronItem(options) {
    if (options.hasOwnProperty('id')) {
        cronItemRunStop(cronJobMaps[options.id]);
        delete cronJobMaps[options.id];
        api_storage_sync_set({ "cron-jobs": cronJobMaps });
    }
}
function cronItemRunStart(item) {
    if (!item.enabled || cronJobStacks.hasOwnProperty(item.id)) return false;
    cronJobStacks[item.id] = {};
    try {
        let openTabToConnect = function (tab) {
            if (!tab) {
                if (item.openType != "open-only") {
                    api_tab_create(item.url, function (tab) {
                        cronJobStacks[item.id]['tab'] = tab;
                        waitTabComplete(tab, 300, function (tab) {
                            item.times++;
                            api_send_tab_message(tab.id, { type: "remote-command-run", item: item }, function (response) {
                                if (item.openType == "finished-close") {
                                    api_tab_remove(tab.id);
                                }
                            })
                        });
                    });
                }
            } else {
                // console.log("api_send_tab_message", tab, {type: "remote-command-run", item: item});
                item.times++;
                api_send_tab_message(tab.id, { type: "remote-command-run", item: item })
            }
        }

        cronJobStacks[item.id]['job'] = new CronJob(item.rule, function () {
            if (cronJobStacks[item.id].hasOwnProperty('tab')) {
                api_tab_get(cronJobStacks[item.id].tab.id, function (tab) {
                    openTabToConnect(tab);
                });
            } else {
                api_tab_query({ url: item.url }, function (tabs) {
                    openTabToConnect(tabs && tabs.length > 0 ? tabs[0] : null);
                });
            }
        });
    } catch (error) {
        console.log(error)
    }
}

function cronItemRunStop(item) {
    if (cronJobStacks.hasOwnProperty(item.id) && cronJobStacks[item.id].hasOwnProperty('job')) {
        cronJobStacks[item.id]['job'].Stop();
        delete cronJobStacks[item.id];
    }
}
function message_rules(message, sender) {
    switch (message.method) {
        case 'getAvailableStaticRuleCount':
            api_get_available_static_rule_count(function(number) {
                api_send_callback_message(sender, message, { 'data': number })
            })
        break;
        case 'getDynamicRules':
            api_get_dynamic_rules(function(rules) {
                api_send_callback_message(sender, message, { 'data': rules })
            })
        break;
        case 'getEnabledRulesets':
            api_get_enabled_rulesets(function(rulesetIds) {
                api_send_callback_message(sender, message, { 'data': rulesetIds })
            })
        break;
        case 'getMatchedRules':
            api_get_matched_rules(message.config, function(details) {
                api_send_callback_message(sender, message, { 'data': details })
            })
        break;
        case 'getSessionRules':
            api_get_session_rules(function(rules) {
                api_send_callback_message(sender, message, { 'data': rules })
            })
        break;
        case 'isRegexSupported':
            api_is_regex_supported(regexOptions, function(result) {
                api_send_callback_message(sender, message, { 'data': result })
            })
        break;
        case 'setExtensionActionOptions':
        break;
        case 'testMatchOutcome':
        break;
        case 'updateDynamicRules':
            api_update_dynamic_rules(message.config, function() {
                api_send_callback_message(sender, message, { 'data': true })
            })
        break;
        case 'updateEnabledRulesets':
            api_update_enabled_rulesets(options, function() {
                // api_send_callback_message(sender, message, { 'data': result })
            })
        break;
        case 'updateSessionRules':
            api_update_session_rules(options, function() {
                // api_send_callback_message(sender, message, { 'data': result })
            })
        break;
    }
}
function extension_handle(message, callback) {
    switch (message.method) {
        case "views":
            extension_get_views_handle(message, callback);
            break;
    }
}

function extension_get_views_handle(callback) {
    api_extension_networkDetails(callback)
}
var tabChan = {};
var responseChan = {};
function response_chan_set(messageId, response) {
    if (!responseChan.hasOwnProperty(messageId)) responseChan[messageId] = [];
    responseChan[messageId].push();
    let messageIds = Object.keys(responseChan);
    if (messageIds.length > 50) delete responseChan[messageIds[0]];
}
function response_chan_get() {

}

// 监听注入页面事件
api_runtime_on_message_listener(function (message, sender, callback) {
    switch (message.type) {
        case "ajax-request":
            fetchios(message.config)
                .then((response) => {
                    api_send_callback_message(sender, message, {
                        data: {
                            status: 200,
                            statusText: "OK",
                            response: response,
                        }
                    });
                })
                .catch((reason) => {
                    api_send_callback_message(sender, message, {
                        data: reason
                    });
                });
            break;
        case "browser-notifications":
            api_notifications_create(message.options, function (result) {
                api_send_callback_message(sender, message, {
                    data: result
                });
            });
            break;
        case "browser-tabs":
            tab_handle(message, function (result) {
                api_send_callback_message(sender, message, {
                    data: result
                });
            });
            break;
        case "browser-bookmarks":
            bookmark_handle(message, function (result) {
                api_send_callback_message(sender, message, {
                    data: result
                });
            });
            break;
        case "browser-history":
            api_history_query(message.options, function (results) {
                api_send_callback_message(sender, message, {
                    data: results
                });
            });
            break;
        case "cron-job":
            let returnMessage = { data: 'ok' };
            if (message.options.type == 'list') {
                returnMessage.data = cronJobMaps;
            } else if (message.options.type == 'add') {
                addCronItem(sender.tab.url, message.options);
            } else if (message.options.type == 'delete') {
                removeCronItem(message.options);
            } else if (message.options.type == 'update') {
                updateCronItem(message.options)
            }
            api_send_callback_message(sender, message, returnMessage);
            break;
        case "exec-remote-command":
            var config = message.config;
            api_tab_query({ url: config.hasOwnProperty('match_url') ? config.match_url : message.site }, function (tabs) {
                if (tabs && tabs.length > 0) {
                    var tab = [];
                    for (let i in tabs) {
                        tab = tabs[i];
                        api_send_tab_message(tabs[i].id, { type: "remote-command-run", item: message.item }, function (result) {
                            api_send_callback_message(sender, message, { 'data': result });
                        });
                        if (!config.hasOwnProperty('exec_all_match_url') || !config.exec_all_match_url) break;
                    }
                    if (config.hasOwnProperty('active_url') && config.active_url) api_tab_update(tab.id, { active: true });
                } else {
                    if (config.hasOwnProperty('auto_open') && config.auto_open) {
                        api_tab_create({ url: message.site, active: config.hasOwnProperty('active_url') ? config.active_url : true }, function (tab) {
                            waitTabComplete(tab, 300, function (tab) {
                                api_send_tab_message(tab.id, { type: "remote-command-run", item: message.item }, function (result) {
                                    api_send_callback_message(sender, message, { 'data': result });
                                });
                            });
                        });
                    }
                }

            });
            break;
        case "exec-remote-tab":
            var config = message.config;
            api_tab_query({ url: config.hasOwnProperty('match_url') ? config.match_url : config.site }, function (tabs) {
                if (tabs && tabs.length > 0) {
                    for (let i in tabs) {
                        api_send_tab_message(tabs[i].id, { type: "remote-tab-run", item: message.item }, function (result) {
                            api_send_callback_message(sender, message, { 'data': result });
                        });
                        break;
                    }
                } else {
                    if (config.hasOwnProperty('auto_open') && config.auto_open) {
                        api_tab_create({ url: config.site, active: config.hasOwnProperty('active_url') ? config.active_url : true }, function (tab) {
                            waitTabComplete(tab, 300, function (tab) {
                                api_send_tab_message(tab.id, { type: "remote-tab-run", item: message.item }, function (result) {
                                    api_send_callback_message(sender, message, { 'data': result });
                                });
                            });
                        });
                    }
                }

            });
            break;
        case "new-tab": 
            api_tab_create(message.url);
        case "cmdhub":
            message_cmdhub(message, sender, callback);
            break;
        case "storage":
            message_storage(message, sender, callback);
            break;
        case "manifest":
            const info = api_manifest()
            api_send_callback_message(sender, message, { 'data': info })
            break;
        case "rule":
            message_rules(message, sender);
            break;
        case "cookie":
            message_cookie(message, sender);
            break;
    }
});

// var storageChangesListenerMessageChannel = {}
// var storageChangesListener = {}
// api_storage_on_change_listener(function(changes, areaName) {
//     for (let key in changes) {
//         if (storageChangesListenerMessageChannel.hasOwnProperty(key)) {
//             storageChangesListenerMessageChannel[key]
//             api_send_tab_message(
//                 storageChangesListenerMessageChannel[key][tab]['id'],
//                 changes[key]['newValue'],
//                 info => {
//                     console.log("selector response:", info)
//                     // callback(info)
//                 }
//             )
//             delete storageChangesListenerMessageChannel[key];
//         } else if (storageChangesListener.hasOwnProperty(key)) {
//             storageChangesListener[key](changes[key]['newValue'], changes[key]['oldValue']);
//         }
//     }
// });

// listener 
api_tab_onupdated(function (tabId, changeInfo, tab) {
    cmdhub_listen_tab_loading_event(tabId, changeInfo, tab);
});

// listener key code event
// chrome.commands.onCommand.addListener(function (command) {
//     if (command == "toggle-cmdwin") {
//         api_tab_current(function (tabs) {
//             try {
//                 // console.log(tabs);
//                 api_send_tab_message(
//                     tabs[0].id,
//                     { type: command },
//                     info => {
//                         if (info == undefined) {
//                             api_tab_create('/main.html');
//                         }
//                         // console.log("callback", info)
//                     }
//                 )
//             } catch (error) {
//                 console.log(error)
//             }
//         });
//     }
// });
chrome.action.onClicked.addListener(function(command) {
    console.log('object', );
        api_tab_current(function (tabs) {
            try {
                // console.log(tabs);
                api_send_tab_message(
                    tabs[0].id,
                    { type: command },
                    info => {
                        if (info == undefined) {
                            api_tab_create('/main.html');
                        }
                        // console.log("callback", info)
                    }
                )
            } catch (error) {
                console.log(error)
            }
        });
});

function waitTabComplete(tab, timeout, callback) {
    if (tab.status == 'loading') {
        setTimeout(() => {
            api_tab_get(tab.id, (tab) => {
                waitTabComplete(tab, timeout + 500, callback)
            });
        }, timeout);
    } else if (tab.status == 'complete') {
        callback(tab)
    }
}
function message_storage(message, sender, callback) {
    var storageKeyPrefix = ((typeof message.space != 'undefined') ? typeof message.space : 'storage')+':';
    let meta = { code: 200, msg: "", data: [] };
    switch (message.method) {
        case 'get':
            var storage_func = api_storage_local_get;
            if (getPropertyVal(message.options, 'area') == 'remote') {
                storage_func = api_storage_sync_get;
            }

            var keys = [];
            for (let i in message.content) {
                keys.push(storageKeyPrefix + message.content[i]);
            }

            storage_func(keys, function (result) {
                meta.data = {};
                if (!checkObjectIsEmpty(result)) {
                    for (let k in result) {
                        meta.data[k.replace(storageKeyPrefix, '')] = result[k];
                    }
                }
                api_send_callback_message(sender, message, { meta: meta });
            });
            break;
        case 'set':
            var data = {};
            var key = '';
            for (let i in message.content) {
                if (i % 2 == 0) {
                    key = message.content[i];
                } else if (key) {
                    data[storageKeyPrefix + key] = message.content[i];
                    key = '';
                }
            }
            var storage_func = api_storage_local_set;
            if (getPropertyVal(message.options, 'area') == 'remote') {
                storage_func = api_storage_sync_set;
            }

            storage_func(data, function () {
                meta.data = true;
                api_send_callback_message(sender, message, { meta: meta });
            });

            break;
    }
}
function tab_handle(message, callback) {
    switch (message.method) {
        case "skip-tab":
            skip_tab_handle(message, callback);
            break;
        case "copy-tab":
            api_tab_copy(null, callback);
            break;
        case "new-tab":
            api_tab_create({}, callback);
            break;
        case "new-win-tab":
            api_window_create({}, callback);
            break;
        case "query":
            tab_query_handle(message, callback);
            break;
        case "update":
            tab_update_handle(message, callback);
            break;
        case "close":
            tab_close_handle(message, callback);
            break;
        case "capture":
            tab_capture_handle(message, callback);
            break;
        case "recently-closed":
            tab_getRecentlyClosed_handle(message, callback);
            break;
        case "restore":
            tab_restore_handle(message, callback);
            break;
    }
}

function skip_tab_handle(message, callback) {
    api_tab_query({ currentWindow: true }, function (tabs) {
        let currentIndex = 0;
        let len = tabs.length;
        for (let i in tabs) {
            if (tabs[i].active) {
                currentIndex = parseInt(i);
                break;
            }
        }

        if (message.options.hasOwnProperty('step')) {
            currentIndex += parseInt(message.options.step);
        } else {
            currentIndex += 1;
        }

        if (currentIndex < 0) {
            currentIndex = len + currentIndex;
        } else if (currentIndex >= len) {
            currentIndex = currentIndex % len;
        }

        api_tab_update(tabs[currentIndex].id, { active: true }, callback);
    })
}

function tab_query_handle(message, callback) {
    api_tab_query(message.options.query, function (tabs) {
        callback(tabs)
    });
}

function tab_update_handle(message, callback) {
    api_window_update(message.options.winId, { focused: true }, function () { })
    api_tab_update(message.options.tabId, message.options.updateProperties, callback);
}

function tab_close_handle(message, callback) {
    if (Object.keys(message.options).length > 0) {
        let queryInfo = {};
        if (message.options.hasOwnProperty('query') ||
            message.options.hasOwnProperty('others') ||
            message.options.hasOwnProperty('toright') ||
            message.options.hasOwnProperty('toleft') ||
            message.options.hasOwnProperty('all')
        ) {
            queryInfo['currentWindow'] = true;
        }
        if (message.options.hasOwnProperty('query')) {
            queryInfo['title'] = '*' + message.options.query + '*';
        }
        if (message.options.hasOwnProperty('query_all')) {
            queryInfo['title'] = '*' + message.options.query_all + '*';
        }
        api_tab_query(queryInfo, function (tabs) {
            if (tabs && tabs.length > 0) {
                if (message.options.hasOwnProperty('others') || message.options.hasOwnProperty('others_all')) {
                    tabs.forEach(function (tab) {
                        if (!tab.active) api_tab_remove(tab.id);
                    });
                } else if (message.options.hasOwnProperty('toright')) {
                    let start = false;
                    tabs.forEach(function (tab) {
                        if (start) api_tab_remove(tab.id);
                        if (tab.active) {
                            start = true;
                        }
                    });
                } else if (message.options.hasOwnProperty('toright_all')) {
                    let start = false;
                    let currentWin = 0;
                    tabs.forEach(function (tab) {
                        if (currentWin != tab.windowId) {
                            currentWin = tab.windowId;
                            start = false;
                        }

                        if (start) api_tab_remove(tab.id);
                        if (tab.active) {
                            start = true;
                        }
                    });
                } else if (message.options.hasOwnProperty('toleft_all')) {
                    let start = true;
                    let currentWin = 0;
                    tabs.forEach(function (tab) {
                        if (currentWin != tab.windowId) {
                            currentWin = tab.windowId;
                            start = true;
                        }
                        if (tab.active) start = false;
                        if (start) api_tab_remove(tab.id);
                    });
                } else if (message.options.hasOwnProperty('toleft')) {
                    let start = true;
                    tabs.forEach(function (tab) {
                        if (tab.active) start = false;
                        if (start) api_tab_remove(tab.id);
                    });
                } else if (message.options.hasOwnProperty('all') || message.options.hasOwnProperty('all_win')) {
                    let tabIds = [];
                    tabs.forEach(function (tab) {
                        tabIds.push(tab.id)
                    });
                    api_tab_remove(tabIds);
                } else if (message.options.hasOwnProperty('query') || message.options.hasOwnProperty('query_all')) {
                    let tabIds = [];
                    tabs.forEach(function (tab) {
                        tabIds.push(tab.id)
                    });
                    api_tab_remove(tabIds);
                }
            }
        })
    } else {
        api_tab_current(function (tabs) {
            if (tabs) {
                api_tab_remove(tabs[0].id, callback)
            }
        })

    }
}

function tab_capture_handle(message, callback) {
    api_tab_captureVisibleTab(callback)
}

function tab_getRecentlyClosed_handle(message, callback) {
    api_sessions_getRecentlyClosed(null, callback);
}

function tab_restore_handle(message, callback) {
    let sessionID = null;
    if (message.content.length > 0) {
        for (let i in message.content) {
            api_sessions_restore(message.content[i], callback);
        }
    } else {
        api_sessions_restore(sessionID, callback);
    }
}